package jp.ddo.trismegistos.container;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jp.ddo.trismegistos.action.Action;
import jp.ddo.trismegistos.dao.DaoManager;
import jp.ddo.trismegistos.enums.Component;
import jp.ddo.trismegistos.exception.ActionContainerException;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

/**
 * Actionクラスを生成するクラス。
 * 
 * @author y_sugasawa
 * @since 2011/05/30
 * 
 */
public class ActionContainer {

	/** ログ */
	private static final Logger log = Logger.getLogger(ActionContainer.class
			.getName());

	/** インスタンス */
	private static final ActionContainer instaince = new ActionContainer();

	/** リクエストURLと生成されるActionクラスのマッピング */
	private Map<String, ActionMapping> actionMap;

	/** サービス名と実装クラスのマッピング */
	private Map<String, ServiceMapping> serviceMap;

	/** DAOクラスのマッピング */
	private Map<String, DaoMapping> daoMap;

	/**
	 * プライベートコンストラクタ。
	 */
	private ActionContainer() {
	}

	/**
	 * ActionContainerクラスのインスタンスを取得する。
	 * 
	 * @return ActionContainerインスタンス
	 */
	public static ActionContainer getInstaince() {
		return instaince;
	}

	/**
	 * Actionを生成する。
	 * 
	 * @param url
	 *            リクエストURL(コンテキスト名以下)
	 * @return Action
	 */
	public Action getAction(final String url, final DaoManager daoManager) {
		final ActionMapping actionMapping = actionMap.get(url);
		if (actionMapping == null) {
			return null;
		}
		Action action = null;
		try {
			// Action生成
			action = (Action) actionMapping.clazz.newInstance();
			// Serviceセット
			setService(action, daoManager,
					actionMapping.componentMap.get(Component.SERVICE));
			// Daoセット
			setDao(action, daoManager,
					actionMapping.componentMap.get(Component.DAO));
		} catch (final Exception e) {
			throw new ActionContainerException("Actionの生成に失敗しました。URL:" + url, e);
		}
		return action;
	}

	/**
	 * Serviceのセットを行う。
	 * 
	 * @param obj
	 *            set対象オブジェクト
	 * @param daoManager
	 *            Daoマネージャー
	 * @param serviceNameSet
	 *            service名のset
	 * @throws Exception
	 */
	protected void setService(final Object obj, final DaoManager daoManager,
			final Set<String> serviceNameSet) throws Exception {
		for (final String serviceName : serviceNameSet) {
			final ServiceMapping serviceMapping = serviceMap.get(serviceName);
			final Object service = serviceMapping.clazz.newInstance();
			setDao(service, daoManager,
					serviceMapping.componentMap.get(Component.DAO));
			// 動くような動かないような・・・。
			// setService(service,
			// serviceMapping.componentMap.get(Component.SERVICE));
			executeSetter(obj, service, serviceMapping.interfaceName,
					serviceMapping.interfaceClass);
		}
	}

	/**
	 * Daoのセットを行う。
	 * 
	 * @param obj
	 *            set対象オブジェクト
	 * @param daoManager
	 *            Daoマネージャー
	 * @param daoNameSet
	 *            Dao名のset
	 * @throws Exception
	 */
	protected void setDao(final Object obj, final DaoManager daoManager,
			final Set<String> daoNameSet) throws Exception {
		for (String daoName : daoNameSet) {
			final DaoMapping daoMapping = daoMap.get(daoName);
			@SuppressWarnings("unchecked")
			final Constructor<DaoManager> constructor = daoMapping.clazz
					.getConstructor(DaoManager.class);
			final Object dao = constructor.newInstance(daoManager);
			executeSetter(obj, dao, daoMapping.interfaceName,
					daoMapping.interfaceClass);
		}
	}

	/**
	 * セッターを呼び出す。
	 * 
	 * @param obj
	 *            呼び出し元オブジェクト
	 * @param setObject
	 *            セットオブジェクト
	 * @param interfaceName
	 *            セッターの引数の型名
	 * @param interfaceClass
	 *            セッターの引数の型
	 * @throws Exception
	 */
	protected void executeSetter(final Object obj, final Object setObject,
			final String interfaceName, final Class interfaceClass)
			throws Exception {
		final Method method = obj.getClass().getDeclaredMethod(
				"set" + interfaceName, interfaceClass);
		method.invoke(obj, setObject);
	}

	/**
	 * 設定ファイルから、Actionクラスのマッピングを作成する。
	 * 
	 * @param filePath
	 *            設定ファイルパス
	 * @exception ActionContainerException
	 */
	public void read(final String filePath) throws ActionContainerException {

		try {
			final Document doc = new SAXBuilder().build(new File(filePath));
			final Element root = doc.getRootElement();

			for (final Component component : Component.values()) {
				createMapping(root, component);
			}
		} catch (final JDOMException e) {
			throw new ActionContainerException(filePath + "の形式が不正です。", e);
		} catch (final ClassNotFoundException e) {
			log.debug(e);
			throw new ActionContainerException(e);
		}
	}

	/**
	 * 各種マッピングを作成する。
	 * 
	 * @param root
	 *            XML要素
	 * @param type
	 *            マッピング作成対象のコンポーネント
	 * @throws ClassNotFoundException
	 *             存在しないクラスがXMLに記載されていた場合
	 */
	protected void createMapping(final Element root, final Component type)
			throws ClassNotFoundException {

		final Element element = root.getChild(type.getType());
		if (element == null) {
			return;
		}
		@SuppressWarnings("unchecked")
		List<Element> list = element.getChildren();
		final String rootPackage = element.getAttributeValue("rootPackage");
		switch (type) {
			case ACTION:
				actionMap = createActionMap(list, rootPackage);
				break;
			case SERVICE:
				serviceMap = createServiceMap(list, rootPackage);
				break;
			case DAO:
				daoMap = createDaoMap(list, rootPackage);
				break;
			default:
				break;
		}
	}

	/**
	 * Actionクラスのマッピングを作成する。
	 * 
	 * @param list
	 *            Actionクラスのリスト
	 * @param actionPackage
	 *            Actionパッケージ名
	 * @return Actionクラスのマッピング
	 * @throws ClassNotFoundException
	 *             存在しないクラスがXMLに記載されていた場合
	 */
	protected Map<String, ActionMapping> createActionMap(
			final List<Element> list, final String actionPackage)
			throws ClassNotFoundException {
		final Map<String, ActionMapping> map = new HashMap<String, ActionMapping>();
		for (final Element e : list) {
			final ActionMapping mapping = new ActionMapping();
			setDefaultValue(mapping, e, actionPackage);
			map.put(createURL(e.getAttributeValue("className")), mapping);
		}

		return Collections.unmodifiableMap(map);
	}

	/**
	 * Serviceクラスのマッピングを作成する。
	 * 
	 * @param list
	 *            クラスのリスト
	 * @param rootPackage
	 *            ルートパッケージ名
	 * @return serviceクラスのマッピング
	 * @throws ClassNotFoundException
	 *             存在しないクラスがXMLに記載されていた場合
	 */
	protected Map<String, ServiceMapping> createServiceMap(
			final List<Element> list, final String rootPackage)
			throws ClassNotFoundException {
		final Map<String, ServiceMapping> map = new HashMap<String, ServiceMapping>();
		for (final Element e : list) {
			final ServiceMapping mapping = new ServiceMapping();
			setDefaultValue(mapping, e, rootPackage);
			final String interfaceName = e.getAttributeValue("interface");
			log.debug("service:" + interfaceName);
			mapping.interfaceClass = Class.forName(rootPackage + "."
					+ interfaceName);
			mapping.interfaceName = interfaceName;
			map.put(e.getAttributeValue("name"), mapping);
		}

		return Collections.unmodifiableMap(map);
	}

	/**
	 * Daoクラスのマッピングを作成する。
	 * 
	 * @param list
	 *            クラスのリスト
	 * @param rootPackage
	 *            ルートパッケージ名
	 * @return Daoクラスのマッピング
	 * @throws ClassNotFoundException
	 *             存在しないクラスがXMLに記載されていた場合
	 */
	protected Map<String, DaoMapping> createDaoMap(final List<Element> list,
			final String rootPackage) throws ClassNotFoundException {
		final Map<String, DaoMapping> map = new HashMap<String, DaoMapping>();
		for (final Element e : list) {
			final DaoMapping mapping = new DaoMapping();
			final String interfaceName = e.getAttributeValue("interface");
			mapping.clazz = Class.forName(rootPackage + "."
					+ e.getAttributeValue("className"));
			mapping.interfaceClass = Class.forName(rootPackage + "."
					+ interfaceName);
			mapping.interfaceName = interfaceName;
			map.put(e.getAttributeValue("name"), mapping);
		}

		return Collections.unmodifiableMap(map);
	}

	/**
	 * ActionMappingクラスの共通項目をセットする。
	 * 
	 * @param mapping
	 *            セット対象クラス
	 * @param e
	 *            エレメント
	 * @param rootPackage
	 *            ルートパッケージ
	 * @throws ClassNotFoundException
	 *             存在しないクラスがXMLに記載されていた場合
	 */
	protected void setDefaultValue(final ActionMapping mapping,
			final Element e, final String rootPackage)
			throws ClassNotFoundException {
		final String className = e.getAttributeValue("className");
		mapping.clazz = Class.forName(rootPackage + "." + className);
		mapping.componentMap.put(Component.SERVICE,
				createChildSet(e, Component.SERVICE.getType()));
		mapping.componentMap.put(Component.DAO,
				createChildSet(e, Component.DAO.getType()));
	}

	/**
	 * Action名からリクエストURLを作成する。
	 * 
	 * @param className
	 *            クラス名
	 * @return
	 */
	protected String createURL(final String className) {
		log.debug("className:" + className);
		return className.replaceAll("[.]", "/").toLowerCase()
				.replaceAll(Component.ACTION.getType(), StringUtils.EMPTY);
	}

	/**
	 * 指定された要素の値のSetを取得する。
	 * 
	 * @param e
	 *            検索元XML
	 * @param childName
	 *            検索要素
	 * @return 値Set
	 */
	protected Set<String> createChildSet(final Element e, final String childName) {
		@SuppressWarnings("unchecked")
		final List<Element> list = e.getChildren(childName);

		final Set<String> set = new HashSet<String>();
		for (final Element el : list) {
			set.add(el.getText());
		}
		return Collections.unmodifiableSet(set);
	}

	private class ActionMapping {
		@SuppressWarnings("rawtypes")
		Class clazz;
		Map<Component, Set<String>> componentMap;

		public ActionMapping() {
			this.componentMap = new HashMap<Component, Set<String>>();
		}
	}

	private class ServiceMapping extends ActionMapping {
		@SuppressWarnings("rawtypes")
		Class interfaceClass;
		String interfaceName;

		public ServiceMapping() {
			super();
		}
	}

	private class DaoMapping {
		@SuppressWarnings("rawtypes")
		Class clazz;
		@SuppressWarnings("rawtypes")
		Class interfaceClass;
		String interfaceName;
	}
}
